package ru.ifmo.ct.network.brafs.server;

import ru.ifmo.ct.network.brafs.IllegalCommandFormatException;
import ru.ifmo.ct.network.brafs.Utils;
import ru.ifmo.ct.network.brafs.node.Command;
import ru.ifmo.ct.network.brafs.node.CommandType;

import java.io.IOException;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.*;

public class Server implements Runnable {
    final int port;
    private static final int REPLICATION = 2;
    private static final long DELAY = 100;

    public Server(int port) {
        this.port = port;
    }

    class Worker implements Runnable {
        final Socket socket;

        Worker(Socket socket) {
            this.socket = socket;
        }

        @Override
        public void run() {
            while (!Thread.interrupted()) {
                final Command command;
                try {
                    command = Command.read(socket.getInputStream());
                } catch (IOException | IllegalCommandFormatException e) {
                    System.err.println("Client disconnected.");
                    return;
                }
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        switch (command.getType()) {
                            case ADD_FILE:
                                addFile(command.getFileName(), command.getSegmentId(), socket.getInetAddress(), command.getPort());
                                break;
                            case DELETE_FILE:
                                deleteFile(command.getFileName());
                                break;
                            case ADD_SEGMENT:
                                addSegment(new Segment(command.getFileName(), command.getSegmentId()), new Client(socket.getInetAddress(), command.getPort()));
                                break;
                            case GET_SEGMENT:
                                sendSegment(new Segment(command.getFileName(), command.getSegmentId()), socket);
                                break;
                            case LIST_FILES:
                                try {
                                    listFile(command.getFileName(), socket.getOutputStream());
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                                break;
                            case HELLO:
                                hello(socket.getInetAddress(), command.getPort());
                                break;
                            default:
                                throw new UnsupportedOperationException();
                        }
                    }

                }).start();
            }
        }
    }

    private void listFile(String filePrefix, OutputStream outputStream) throws IOException {
        System.err.println("listFile " + filePrefix + " " + outputStream);
        int n = 0;
        int len = 4;
        for (String file : files.keySet()) {
            if (file.startsWith(filePrefix)) {
                len += 4 + file.length() * 3;
                n++;
            }
        }
        ByteBuffer bb = ByteBuffer.allocate(len);
        bb.putInt(n);
        for (String file : files.keySet()) {
            if (file.startsWith(filePrefix)) {
                bb.putInt(file.length());
                bb.put(file.getBytes(Charset.forName("utf-8")));
            }
        }
        outputStream.write(bb.array());
        outputStream.flush();
    }

    private void hello(InetAddress inetAddress, int port) {
        System.err.println("hello " + inetAddress + " " + port);
        ClientId clientId = new ClientId(inetAddress, port);
        if (!clients.containsKey(clientId)) {
            clients.put(clientId, new Client(inetAddress, port));
        }
        clients.get(clientId).keepAlive();
    }

    private void addSegment(Segment segment, Client sourceClient) {
        System.err.println("addSegment " + segment + " " + sourceClient);
        if (!files.containsKey(segment.filename)) {
            files.put(segment.filename, new ArrayList<Segment>());
        }
        files.get(segment.filename).add(segment);
        for (int i = 0; i < REPLICATION; i++) {
            Client client = getRandomClient();
            try {
                client.getSegmentFrom(sourceClient, segment);
                while (!client.hasSegment(segment)) {
                    try {
                        Thread.sleep(DELAY);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                sourceClient = client;
                segment.addClient(client);
            } catch (IOException e) {
                e.printStackTrace();
                client.somethingBadHappens();
                i--;
            }
        }
    }

    private void sendSegment(Segment segment, Socket client) {
        for (Segment addedSegment : files.get(segment.filename)) {
            if (addedSegment.equals(segment)) {
                segment = addedSegment;
                System.err.println("Sending" + segment);
                for (Client sourceClient : segment.getClients()) {
                    try {
                        if (sourceClient.hasSegment(segment)) {
                            System.err.println("Sending from" + sourceClient);
                            new Command(CommandType.GET_SEGMENT_FROM, sourceClient.getIp(), sourceClient.port,
                                    segment.id, segment.filename).write(client.getOutputStream());
                            client.getOutputStream().flush();
                            return;
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        try {
            new Command(CommandType.NO_MORE_SEGMENTS, 0, "").write(client.getOutputStream());
            client.getOutputStream().flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private Client getRandomClient() {
        ClientId[] clientIds = clients.keySet().toArray(new ClientId[clients.size()]);
        return clients.get(clientIds[random.nextInt(clientIds.length)]);
        //Speed up!
    }

    private void deleteFile(String fileName) {
        for (Segment segment : files.get(fileName)) {
            for (Client client : segment.getClients()) {
                try {
                    client.removeSegment(segment);
                } catch (IOException e) {
                    client.somethingBadHappens();
                    e.printStackTrace();
                }
            }
        }
    }

    private void addFile(final String fileName, long segmentCnt, final InetAddress inetAddress, final int port) {
        for (int segmentId = 0; segmentId < segmentCnt; segmentId++) {
            final int finalSegmentId = segmentId;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    addSegment(new Segment(fileName, finalSegmentId), new Client(inetAddress, port));
                }
            }).start();
            //Use threadpool
        }
    }

    Map<String, List<Segment>> files = new HashMap<>();
    Map<ClientId, Client> clients = new HashMap<>();
    Random random = new Random(1);

    public void run() {
        try {
            ServerSocket serverSocket = new ServerSocket(port);
            while (!Thread.interrupted()) {
                Socket socket = serverSocket.accept();
                new Thread(new Worker(socket)).start();
            }
            serverSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public class Client extends ClientId {

        private Socket socket;

        public Client(InetAddress inetAddress, int port) {
            super(inetAddress, port);
        }

        public void keepAlive() {
            //Do something
        }

        public void somethingBadHappens() {
            /*clients.remove(this);
            for (List<Segment> ls : files.values()) {
                for (Segment s : ls) {
                    if (s.clients.contains(this)) {
                        addSegment(s, s.getOtherClient(this));
                    }
                    s.clients.remove(this);
                }
            }*/

        }

        synchronized void openSocket() throws IOException {
            if (socket == null) {
                socket = new Socket(inetAddress, port);
            }
        }

        synchronized public void removeSegment(Segment segment) throws IOException {
            openSocket();
            new Command(CommandType.DELETE_SEGMENT, segment.id, segment.filename).write(socket.getOutputStream());
            socket.getOutputStream().flush();
            closeSocket();
        }

        public void getSegmentFrom(Client sourceClient, Segment segment) throws IOException {
            openSocket();
            new Command(CommandType.GET_SEGMENT_FROM, sourceClient.getIp(), sourceClient.port, segment.id, segment.filename).write(socket.getOutputStream());
            socket.getOutputStream().flush();
            closeSocket();
        }


        public boolean hasSegment(Segment segment) throws IOException {
            openSocket();
            new Command(CommandType.CHECK_SEGMENT, segment.id, segment.filename).write(socket.getOutputStream());
            socket.getOutputStream().flush();
            int i = Utils.readInt(socket.getInputStream());
//            System.err.println("i = " + i);
            closeSocket();
            return i != 0;
        }

        synchronized private void closeSocket() throws IOException {
            /*if (socket != null) {
                try {
                    socket.close();
                } finally {
                    socket = null;
                }
            }*/

        }

        @Override
        public String toString() {
            return "Client{" +
                    "socket=" + socket +
                    '}' + super.toString();
        }
    }

    public static void main(String[] args) {
        new Server(Integer.parseInt(args[0])).run();
    }
}
